package wailsutil

import (
	"archive/zip"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"golang.org/x/text/encoding/simplifiedchinese"
	"golang.org/x/text/transform"
	"gopkg.in/ini.v1"
)

var (
	programName      = ""
	serverVersionURL = ""
	downloadURL      = ""
	targetPath       = ""
	notice           = false            //是否通知前端
	checkInterval    = 60 * time.Second // 每60秒检查一次
)

/**
 * 自动更新
 * @param configFile
 */
func AutoUpdate(configFile string, execJS func(jsCode string)) {
	cfg, err := ini.Load(configFile)
	if err != nil {
		clogger.Error("无法加载配置文件: " + err.Error())
		return
	}

	temp := Read(cfg, "AutoUpate", "Open")
	if temp == "" {
		clogger.Info("自动更新未开启")
		return
	}

	open, err := strconv.Atoi(temp)
	if open != 1 || err != nil {
		clogger.Info("自动更新未开启")
		return
	}

	currentVersion := Read(cfg, "AutoUpate", "Version")
	programName = Read(cfg, "AutoUpate", "Program")
	serverVersionURL = Read(cfg, "AutoUpate", "VersionURL")
	downloadURL = Read(cfg, "AutoUpate", "DownloadURL")
	notice = Read(cfg, "AutoUpate", "Notice") == "1"
	targetPath = Read(cfg, "AutoUpate", "TargetPath")
	targetPath = strings.TrimSuffix(targetPath, "/")

	temp = Read(cfg, "AutoUpate", "CheckInterval")
	if temp != "" {
		tempInt, err := strconv.Atoi(temp)
		if err == nil {
			checkInterval = time.Duration(tempInt) * time.Minute
		}
	}

	clogger.Debug("当前版本:" + currentVersion)

	ticker := time.NewTicker(checkInterval)
	defer ticker.Stop()

	clogger.Debug("开始检查版本")
	for range ticker.C {
		serverVersion, err := getServerVersion()
		if err != nil {
			clogger.Error("无法获取服务器版本:" + err.Error())
			continue
		}

		if serverVersion == currentVersion {
			continue
		}

		clogger.Debug("发现新版本:" + serverVersion)
		if err := updateToVersion(serverVersion); err != nil {
			clogger.Error("更新失败: " + err.Error())
			continue
		}

		clogger.Info("更新成功，新版本:" + serverVersion)
		currentVersion = serverVersion
		cfg.Section("AutoUpate").Key("Version").SetValue(serverVersion)
		cfg.SaveTo(configFile)

		// 通知前端更新完成
		if notice {
			execJS("window.ShowMsgByGo('新版本已经准备就绪，重启程序起效！');") // 通知前端更新完成
		}
	}
}

/**
 * 获取服务器版本
 * @return string
 */
func getServerVersion() (string, error) {
	resp, err := http.Get(fmt.Sprintf("%s?program=%s", serverVersionURL, programName))
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	me := map[string]interface{}{}
	err = json.Unmarshal(body, &me)
	if err != nil {
		return "", err
	}

	if !me["success"].(bool) {
		return "", fmt.Errorf(me["msg"].(string))
	}

	return me["data"].(string), nil
}

/**
 * 更新软件到指定版本
 * @param version
 * @return error
 */
func updateToVersion(version string) error {
	unzipPath := strings.Replace(targetPath, "{NewVersion}", version, -1) + "/" //如果有版本号，则说明按版本号建立目录
	unzipPath = strings.Replace(unzipPath, "//", "/", -1)
	downloadPath := fmt.Sprintf("%s%s.zip", unzipPath, version)
	checksumPath := fmt.Sprintf("%s%s_SHA256.txt", unzipPath, version)

	// 下载文件
	if err := downloadFile(downloadPath, fmt.Sprintf("%s?program=%s&version=%s", downloadURL, programName, version)); err != nil {
		return err
	}

	// 下载校验文件
	if err := downloadFile(checksumPath, fmt.Sprintf("%s?program=%s&version=%s&checksum=1", downloadURL, programName, version)); err != nil {
		return err
	}

	// 检查文件完整性
	if !checkFileIntegrity(downloadPath, checksumPath) {
		return fmt.Errorf("文件完整性检查失败")
	}

	// 解压文件
	if err := unzip(downloadPath, unzipPath); err != nil {
		return err
	}

	// 删除下载的压缩文件和校验文件
	if err := os.Remove(downloadPath); err != nil {
		return err
	}
	if err := os.Remove(checksumPath); err != nil {
		return err
	}

	return nil
}

/**
 * 下载文件
 * @param path
 * @param url
 * @return error
 */
func downloadFile(path string, url string) error {
	resp, err := http.Get(url)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	dir := filepath.Dir(path)
	err = os.MkdirAll(dir, 0755)
	if err != nil {
		return err
	}

	out, err := os.Create(path)
	if err != nil {
		return err
	}
	defer out.Close()

	_, err = io.Copy(out, resp.Body)
	return err
}

/**
 * 检查文件完整性
 * @param filepath
 * @param checksumPath
 * @return bool
 */
func checkFileIntegrity(filepath, checksumPath string) bool {
	// 读取校验文件中的预期校验和
	expectedChecksum, err := os.ReadFile(checksumPath)
	if err != nil {
		clogger.Error("无法读取校验文件: " + err.Error())
		return false
	}

	// 计算文件的实际校验和
	actualChecksum, err := calculateSHA256(filepath)
	if err != nil {
		clogger.Error("无法计算文件校验和: " + err.Error())
		return false
	}

	return actualChecksum == string(expectedChecksum)
}

func calculateSHA256(filepath string) (string, error) {
	data, err := os.ReadFile(filepath)
	if err != nil {
		return "", err
	}

	hash := sha256.Sum256(data)
	actualChecksum := hex.EncodeToString(hash[:])

	return actualChecksum, nil
}

/**
 * 解压文件
 * @param src
 * @param dest
 * @return error
 */
func unzip(src, dest string) error {
	reader, err := zip.OpenReader(src)
	if err != nil {
		return err
	}
	defer reader.Close()

	for _, file := range reader.File {
		// 检测并转换文件名编码
		decodedFileName, err := decodeFileName(file.Name)
		if err != nil {
			//log.Printf("无法解码文件名: %v, 使用原始文件名: %s", err, file.Name)
			decodedFileName = file.Name
		}

		filePath := filepath.Join(dest, decodedFileName)
		if file.FileInfo().IsDir() {
			os.MkdirAll(filePath, file.Mode())
			continue
		}

		filePath = filepath.Join(dest, decodedFileName)
		os.MkdirAll(filepath.Dir(filePath), file.Mode())

		outFile, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
		if err != nil {
			return err
		}
		defer outFile.Close()

		rc, err := file.Open()
		if err != nil {
			return err
		}
		defer rc.Close()

		_, err = io.Copy(outFile, rc)
		if err != nil {
			return err
		}
	}

	return nil
}

/**
 * 尝试使用 GBK 解码文件名
 * @param fileName
 * @return string
 */
func decodeFileName(fileName string) (string, error) {
	decodedBytes, err := io.ReadAll(transform.NewReader(strings.NewReader(fileName), simplifiedchinese.GBK.NewDecoder()))
	if err != nil {
		return "", err
	}
	return string(decodedBytes), nil
}
